/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { AbstractInvokeExpr, ANONYMOUS_METHOD_PREFIX, ArkAssignStmt, ArkInstanceInvokeExpr, ArkMethod, ImportInfo, Local, Stmt } from 'arkanalyzer/lib';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule, Defects, MatcherTypes, MethodMatcher, MatcherCallback, CheckerUtils } from '../../Index';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'BadDeepCloneCheck');
const lodashCloneDeepFunctions = [
    '_.cloneDeep',
    'lodash.cloneDeep'
];
const ParseSign: string[] = [
    `@internalSdk/@internal/lib.es5.d.ts: JSON.parse(string, @internalSdk/@internal/lib.es5.d.ts: JSON.${ANONYMOUS_METHOD_PREFIX}0(any, string, any))`,
    `@internalSdk/@internal/es5.d.ts: JSON.parse(string, @internalSdk/@internal/es5.d.ts: JSON.${ANONYMOUS_METHOD_PREFIX}0(any, string, any))`
];
const StringifySign: string[] = [
    `@internalSdk/@internal/lib.es5.d.ts: JSON.stringify(any, @internalSdk/@internal/lib.es5.d.ts: JSON.${ANONYMOUS_METHOD_PREFIX}1(any, string, any), string|number)`,
    `@internalSdk/@internal/es5.d.ts: JSON.stringify(any, @internalSdk/@internal/es5.d.ts: JSON.${ANONYMOUS_METHOD_PREFIX}1(any, string, any), string|number)`
];
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: 'docs/bad-deep-clone-check.md',
    description: 'Unreasonable deep copy detection: JSON.parse(JSON.stringify(...)) and _.cloneDeep(...).'
};

interface RuleOptions {
    functions?: string[];
}

interface Function {
    objectName: string | null;
    methodName: string;
}

export class BadDeepCloneCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private mtdMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
    };

    public registerMatchers(): MatcherCallback[] {
        const matchMethodCb: MatcherCallback = {
            matcher: this.mtdMatcher,
            callback: this.check
        };
        return [matchMethodCb];
    }

    public check = (target: ArkMethod): void => {
        const configFunctions = (this.rule.option[0] as RuleOptions | undefined)?.functions ?? [];
        const allFunctions = [...lodashCloneDeepFunctions, ...configFunctions];
        const importInfos = target.getDeclaringArkFile().getImportInfos();
        const functions: Function[] = this.processFunctions(allFunctions);
        const importNames: string[] = this.processImportInfos(importInfos);
        const stmts = target.getBody()?.getCfg().getStmts() ?? [];
        for (let stmt of stmts) {
            const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokeExpr) {
                continue;
            }
            const methodSignStr = invokeExpr.getMethodSignature().toString();
            if (ParseSign.includes(methodSignStr)) {
                this.processJson(stmt, invokeExpr);
            } else {
                this.processMethod(stmt, invokeExpr, functions, importNames);
            }
        }
    };

    private processFunctions(allFunctions: string[]): Function[] {
        const functions: Function[] = [];
        for (let allFunction of allFunctions) {
            if (allFunction.includes('.')) {
                const [objectName, methodName] = allFunction.split('.');
                functions.push({ objectName: objectName, methodName: methodName });
            } else {
                functions.push({ objectName: null, methodName: allFunction });
            }
        }
        return functions;
    }

    private processImportInfos(importInfos: ImportInfo[]): string[] {
        const importNames: string[] = [];
        for (let importInfo of importInfos) {
            const name = importInfo.getImportClauseName();
            importNames.push(name);
        }
        return importNames;
    }

    private processJson(stmt: Stmt, invokeExpr: AbstractInvokeExpr): void {
        const arg = invokeExpr.getArg(0);
        if (!arg) {
            return;
        }
        if (!(arg instanceof Local)) {
            return;
        }
        const declaringStmt = arg.getDeclaringStmt();
        if (!(declaringStmt instanceof ArkAssignStmt)) {
            return;
        }
        const rightOp = declaringStmt.getRightOp();
        if (!(rightOp instanceof ArkInstanceInvokeExpr)) {
            return;
        }
        const methodSignStr2 = rightOp.getMethodSignature().toString();
        if (!StringifySign.includes(methodSignStr2)) {
            return;
        }
        this.reportIssue(stmt, 'parse');
    }

    private processMethod(stmt: Stmt, invokeExpr: AbstractInvokeExpr, functions: Function[], importNames: string[]): void {
        for (let [key, value] of Object.entries(functions)) {
            const methodName = invokeExpr.getMethodSignature().getMethodSubSignature().getMethodName();
            if (value.methodName !== methodName) {
                continue;
            }
            if (!(invokeExpr instanceof ArkInstanceInvokeExpr)) {
                continue;
            }
            const args = invokeExpr.getArgs();
            if (args.length !== 1) {
                continue;
            }
            if (value.objectName) {
                const baseName = invokeExpr.getBase().getName();
                if (value.objectName !== baseName) {
                    continue;
                }
                if (!importNames.includes(baseName)) {
                    continue;
                }
            }
            this.reportIssue(stmt, methodName);
            return;
        }
    }

    private reportIssue(stmt: Stmt, key: string): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile();
        if (!arkFile) {
            return;
        }
        let lineCount = -1;
        let startColumn = -1;
        let endColunm = -1;
        const filePath = arkFile.getFilePath();
        const originPosition = stmt.getOriginPositionInfo();
        let lineNum = originPosition.getLineNo();
        const originText = stmt.getOriginalText();
        if (!originText || originText.length === 0) {
            return;
        }
        if (originText.includes('\r\n')) {
            let originTexts = originText.split('\n');
            for (let originText of originTexts) {
                lineCount++;
                if (!originText.includes(key)) {
                    continue;
                }
                if (lineCount === 0) {
                    startColumn = originText.indexOf(key) + originPosition.getColNo();
                } else {
                    startColumn = originText.indexOf(key) + 1;
                }
                endColunm = startColumn + key.length - 1;
                lineNum = lineNum + lineCount;
            }
        } else {
            startColumn = originPosition.getColNo() + originText.indexOf(key);
            endColunm = startColumn + key.length - 1;
        }
        let defects = new Defects(lineNum, startColumn, endColunm, this.metaData.description, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}